Passed
Branch wavefile-rw (a1d6f9)
by Rafael S.
02:39
created

WaveFile.createPCMHeader_   A

Complexity

Conditions 1

Size

Total Lines 20
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 18
dl 0
loc 20
rs 9.5
c 0
b 0
f 0
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import bitDepthLib from 'bitdepth';
33
import * as imaadpcm from 'imaadpcm';
34
import * as alawmulaw from 'alawmulaw';
35
import {encode, decode} from 'base64-arraybuffer-es6';
36
import WaveFileCreator from './lib/wavefile-creator';
37
import truncateSamples from './lib/truncate-samples';
38
import fixRIFFTag from './lib/fix-riff-tag';
39
import {unpackArray, unpackArrayTo, unpack, packTo} from 'byte-data';
40
41
/**
42
 * A class to manipulate wav files.
43
 */
44
export default class WaveFile extends WaveFileCreator {
45
46
  /**
47
   * @param {?Uint8Array=} wavBuffer A wave file buffer.
48
   * @throws {Error} If container is not RIFF, RIFX or RF64.
49
   * @throws {Error} If format is not WAVE.
50
   * @throws {Error} If no 'fmt ' chunk is found.
51
   * @throws {Error} If no 'data' chunk is found.
52
   */
53
  constructor(wavBuffer=null) {
54
    super();
55
    // Load a file from the buffer if one was passed
56
    // when creating the object
57
    if (wavBuffer) {
58
      this.fromBuffer(wavBuffer);
59
    }
60
  }
61
62
  /**
63
   * Set up the WaveFile object based on the arguments passed.
64
   * @param {number} numChannels The number of channels
65
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
66
   * @param {number} sampleRate The sample rate.
67
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
68
   * @param {string} bitDepthCode The audio bit depth code.
69
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
70
   *    or any value between '8' and '32' (like '12').
71
   * @param {!Array<number>|!Array<!Array<number>>|!TypedArray} samples
72
   *    The samples. Must be in the correct range according to the bit depth.
73
   * @param {?Object} options Optional. Used to force the container
74
   *    as RIFX with {'container': 'RIFX'}
75
   * @throws {Error} If any argument does not meet the criteria.
76
   */
77
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
78
    this.create(numChannels, sampleRate, bitDepthCode, samples, options);
79
  }
80
81
  /**
82
   * Set up the WaveFile object from a byte buffer.
83
   * @param {!Uint8Array} wavBuffer The buffer.
84
   * @param {boolean=} samples True if the samples should be loaded.
85
   * @throws {Error} If container is not RIFF, RIFX or RF64.
86
   * @throws {Error} If format is not WAVE.
87
   * @throws {Error} If no 'fmt ' chunk is found.
88
   * @throws {Error} If no 'data' chunk is found.
89
   */
90
  fromBuffer(wavBuffer, samples=true) {
91
    this.readBuffer(wavBuffer, samples);
92
  }
93
94
  /**
95
   * Return a byte buffer representig the WaveFile object as a .wav file.
96
   * The return value of this method can be written straight to disk.
97
   * @return {!Uint8Array} A wav file.
98
   * @throws {Error} If bit depth is invalid.
99
   * @throws {Error} If the number of channels is invalid.
100
   * @throws {Error} If the sample rate is invalid.
101
   */
102
  toBuffer() {
103
    return this.writeBuffer();
104
  }
105
106
  /**
107
   * Force a file as RIFF.
108
   */
109
  toRIFF() {
110
    this.fromScratch(
111
      this.fmt.numChannels,
112
      this.fmt.sampleRate,
113
      this.bitDepth,
114
      unpackArray(this.data.samples, this.dataType));
115
  }
116
117
  /**
118
   * Force a file as RIFX.
119
   */
120
  toRIFX() {
121
    this.fromScratch(
122
      this.fmt.numChannels,
123
      this.fmt.sampleRate,
124
      this.bitDepth,
125
      unpackArray(this.data.samples, this.dataType),
126
      {container: 'RIFX'});
127
  }
128
129
  /**
130
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
131
   * @throws {Error} If sample rate is not 8000.
132
   * @throws {Error} If number of channels is not 1.
133
   */
134
  toIMAADPCM() {
135
    if (this.fmt.sampleRate !== 8000) {
136
      throw new Error(
137
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
138
    } else if (this.fmt.numChannels !== 1) {
139
      throw new Error(
140
        'Only mono files can be compressed as IMA-ADPCM.');
141
    } else {
142
      this.assure16Bit_();
143
      /** @type {!Int16Array} */
144
      let output = new Int16Array(this.data.samples.length / 2);
145
      unpackArrayTo(this.data.samples, this.dataType, output);
146
      this.fromScratch(
147
        this.fmt.numChannels,
148
        this.fmt.sampleRate,
149
        '4',
150
        imaadpcm.encode(output),
151
        {container: this.correctContainer_()});
152
    }
153
  }
154
155
  /**
156
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
157
   * @param {string} bitDepthCode The new bit depth of the samples.
158
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
159
   *    Optional. Default is 16.
160
   */
161
  fromIMAADPCM(bitDepthCode='16') {
162
    this.fromScratch(
163
      this.fmt.numChannels,
164
      this.fmt.sampleRate,
165
      '16',
166
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
167
      {container: this.correctContainer_()});
168
    if (bitDepthCode != '16') {
169
      this.toBitDepth(bitDepthCode);
170
    }
171
  }
172
173
  /**
174
   * Encode a 16-bit wave file as 8-bit A-Law.
175
   */
176
  toALaw() {
177
    this.assure16Bit_();
178
    /** @type {!Int16Array} */
179
    let output = new Int16Array(this.data.samples.length / 2);
180
    unpackArrayTo(this.data.samples, this.dataType, output);
181
    this.fromScratch(
182
      this.fmt.numChannels,
183
      this.fmt.sampleRate,
184
      '8a',
185
      alawmulaw.alaw.encode(output),
186
      {container: this.correctContainer_()});
187
  }
188
189
  /**
190
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
191
   * @param {string} bitDepthCode The new bit depth of the samples.
192
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
193
   *    Optional. Default is 16.
194
   */
195
  fromALaw(bitDepthCode='16') {
196
    this.fromScratch(
197
      this.fmt.numChannels,
198
      this.fmt.sampleRate,
199
      '16',
200
      alawmulaw.alaw.decode(this.data.samples),
201
      {container: this.correctContainer_()});
202
    if (bitDepthCode != '16') {
203
      this.toBitDepth(bitDepthCode);
204
    }
205
  }
206
207
  /**
208
   * Encode 16-bit wave file as 8-bit mu-Law.
209
   */
210
  toMuLaw() {
211
    this.assure16Bit_();
212
    /** @type {!Int16Array} */
213
    let output = new Int16Array(this.data.samples.length / 2);
214
    unpackArrayTo(this.data.samples, this.dataType, output);
215
    this.fromScratch(
216
      this.fmt.numChannels,
217
      this.fmt.sampleRate,
218
      '8m',
219
      alawmulaw.mulaw.encode(output),
220
      {container: this.correctContainer_()});
221
  }
222
223
  /**
224
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
225
   * @param {string} bitDepthCode The new bit depth of the samples.
226
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
227
   *    Optional. Default is 16.
228
   */
229
  fromMuLaw(bitDepthCode='16') {
230
    this.fromScratch(
231
      this.fmt.numChannels,
232
      this.fmt.sampleRate,
233
      '16',
234
      alawmulaw.mulaw.decode(this.data.samples),
235
      {container: this.correctContainer_()});
236
    if (bitDepthCode != '16') {
237
      this.toBitDepth(bitDepthCode);
238
    }
239
  }
240
241
  /**
242
   * Return the sample at a given index.
243
   * @param {number} index The sample index.
244
   * @return {number} The sample.
245
   * @throws {Error} If the sample index is off range.
246
   */
247
  getSample(index) {
248
    index = index * (this.dataType.bits / 8);
249
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
250
      throw new Error('Range error');
251
    }
252
    return unpack(
253
      this.data.samples.slice(index, index + this.dataType.bits / 8),
254
      this.dataType);
255
  }
256
257
  /**
258
   * Set the sample at a given index.
259
   * @param {number} index The sample index.
260
   * @param {number} sample The sample.
261
   * @throws {Error} If the sample index is off range.
262
   */
263
  setSample(index, sample) {
264
    index = index * (this.dataType.bits / 8);
265
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
266
      throw new Error('Range error');
267
    }
268
    packTo(sample, this.dataType, this.data.samples, index);
269
  }
270
271
  /**
272
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
273
   * @param {string} base64String A .wav file as a base64 string.
274
   * @throws {Error} If any property of the object appears invalid.
275
   */
276
  fromBase64(base64String) {
277
    this.fromBuffer(new Uint8Array(decode(base64String)));
278
  }
279
280
  /**
281
   * Return a base64 string representig the WaveFile object as a .wav file.
282
   * @return {string} A .wav file as a base64 string.
283
   * @throws {Error} If any property of the object appears invalid.
284
   */
285
  toBase64() {
286
    /** @type {!Uint8Array} */
287
    let buffer = this.toBuffer();
288
    return encode(buffer, 0, buffer.length);
289
  }
290
291
  /**
292
   * Return a DataURI string representig the WaveFile object as a .wav file.
293
   * The return of this method can be used to load the audio in browsers.
294
   * @return {string} A .wav file as a DataURI.
295
   * @throws {Error} If any property of the object appears invalid.
296
   */
297
  toDataURI() {
298
    return 'data:audio/wav;base64,' + this.toBase64();
299
  }
300
301
  /**
302
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
303
   * @param {string} dataURI A .wav file as DataURI.
304
   * @throws {Error} If any property of the object appears invalid.
305
   */
306
  fromDataURI(dataURI) {
307
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
308
  }
309
310
  /**
311
   * Change the bit depth of the samples.
312
   * @param {string} newBitDepth The new bit depth of the samples.
313
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
314
   * @param {boolean} changeResolution A boolean indicating if the
315
   *    resolution of samples should be actually changed or not.
316
   * @throws {Error} If the bit depth is not valid.
317
   */
318
  toBitDepth(newBitDepth, changeResolution=true) {
319
    /** @type {string} */
320
    let toBitDepth = newBitDepth;
321
    /** @type {string} */
322
    let thisBitDepth = this.bitDepth;
323
    if (!changeResolution) {
324
      if (newBitDepth != '32f') {
325
        toBitDepth = this.dataType.bits.toString();
326
      }
327
      thisBitDepth = this.dataType.bits;
328
    }
329
    this.assureUncompressed_();
330
    /** @type {number} */
331
    let sampleCount = this.data.samples.length / (this.dataType.bits / 8);
332
    /** @type {!Float64Array} */
333
    let typedSamplesInput = new Float64Array(sampleCount);
334
    /** @type {!Float64Array} */
335
    let typedSamplesOutput = new Float64Array(sampleCount);
336
    unpackArrayTo(this.data.samples, this.dataType, typedSamplesInput);
337
    if (thisBitDepth == "32f" || thisBitDepth == "64") {
338
      truncateSamples(typedSamplesInput);
339
    }
340
    bitDepthLib(
341
      typedSamplesInput, thisBitDepth, toBitDepth, typedSamplesOutput);
342
    this.fromScratch(
343
      this.fmt.numChannels,
344
      this.fmt.sampleRate,
345
      newBitDepth,
346
      typedSamplesOutput,
347
      {container: this.correctContainer_()});
348
  }
349
350
  /**
351
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
352
   * then it is created. It if exists, it is overwritten.
353
   * @param {string} tag The tag name.
354
   * @param {string} value The tag value.
355
   * @throws {Error} If the tag name is not valid.
356
   */
357
  setTag(tag, value) {
358
    tag = fixRIFFTag(tag);
359
    /** @type {!Object} */
360
    let index = this.getTagIndex_(tag);
361
    if (index.TAG !== null) {
362
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
363
        value.length + 1;
364
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
365
    } else if (index.LIST !== null) {
366
      this.LIST[index.LIST].subChunks.push({
367
        chunkId: tag,
368
        chunkSize: value.length + 1,
369
        value: value});
370
    } else {
371
      this.LIST.push({
372
        chunkId: 'LIST',
373
        chunkSize: 8 + value.length + 1,
374
        format: 'INFO',
375
        subChunks: []});
376
      this.LIST[this.LIST.length - 1].subChunks.push({
377
        chunkId: tag,
378
        chunkSize: value.length + 1,
379
        value: value});
380
    }
381
  }
382
383
  /**
384
   * Return the value of a RIFF tag in the INFO chunk.
385
   * @param {string} tag The tag name.
386
   * @return {?string} The value if the tag is found, null otherwise.
387
   */
388
  getTag(tag) {
389
    /** @type {!Object} */
390
    let index = this.getTagIndex_(tag);
391
    if (index.TAG !== null) {
392
      return this.LIST[index.LIST].subChunks[index.TAG].value;
393
    }
394
    return null;
395
  }
396
397
  /**
398
   * Return a Object<tag, value> with the RIFF tags in the file.
399
   * @return {!Object<string, string>} The file tags.
400
   */
401
  listTags() {
402
    /** @type {?number} */
403
    let index = this.getLISTINFOIndex_();
404
    /** @type {!Object} */
405
    let tags = {};
406
    if (index !== null) {
407
      for (let i = 0, len = this.LIST[index].subChunks.length; i < len; i++) {
408
        tags[this.LIST[index].subChunks[i].chunkId] =
409
          this.LIST[index].subChunks[i].value;
410
      }
411
    }
412
    return tags;
413
  }
414
415
  /**
416
   * Remove a RIFF tag from the INFO chunk.
417
   * @param {string} tag The tag name.
418
   * @return {boolean} True if a tag was deleted.
419
   */
420
  deleteTag(tag) {
421
    /** @type {!Object} */
422
    let index = this.getTagIndex_(tag);
423
    if (index.TAG !== null) {
424
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
425
      return true;
426
    }
427
    return false;
428
  }
429
430
  /**
431
   * Create a cue point in the wave file.
432
   * @param {number} position The cue point position in milliseconds.
433
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
434
   */
435
  setCuePoint(position, labl='') {
436
    this.cue.chunkId = 'cue ';
437
    position = (position * this.fmt.sampleRate) / 1000;
438
    /** @type {!Array<!Object>} */
439
    let existingPoints = this.getCuePoints_();
440
    this.clearLISTadtl_();
441
    /** @type {number} */
442
    let len = this.cue.points.length;
443
    this.cue.points = [];
444
    /** @type {boolean} */
445
    let hasSet = false;
446
    if (len === 0) {
447
      this.setCuePoint_(position, 1, labl);
448
    } else {
449
      for (let i = 0; i < len; i++) {
450
        if (existingPoints[i].dwPosition > position && !hasSet) {
451
          this.setCuePoint_(position, i + 1, labl);
452
          this.setCuePoint_(
453
            existingPoints[i].dwPosition,
454
            i + 2,
455
            existingPoints[i].label);
456
          hasSet = true;
457
        } else {
458
          this.setCuePoint_(
459
            existingPoints[i].dwPosition,
460
            i + 1,
461
            existingPoints[i].label);
462
        }
463
      }
464
      if (!hasSet) {
465
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
466
      }
467
    }
468
    this.cue.dwCuePoints = this.cue.points.length;
469
  }
470
471
  /**
472
   * Remove a cue point from a wave file.
473
   * @param {number} index the index of the point. First is 1,
474
   *    second is 2, and so on.
475
   */
476
  deleteCuePoint(index) {
477
    this.cue.chunkId = 'cue ';
478
    /** @type {!Array<!Object>} */
479
    let existingPoints = this.getCuePoints_();
480
    this.clearLISTadtl_();
481
    /** @type {number} */
482
    let len = this.cue.points.length;
483
    this.cue.points = [];
484
    for (let i = 0; i < len; i++) {
485
      if (i + 1 !== index) {
486
        this.setCuePoint_(
487
          existingPoints[i].dwPosition,
488
          i + 1,
489
          existingPoints[i].label);
490
      }
491
    }
492
    this.cue.dwCuePoints = this.cue.points.length;
493
    if (this.cue.dwCuePoints) {
494
      this.cue.chunkId = 'cue ';
495
    } else {
496
      this.cue.chunkId = '';
497
      this.clearLISTadtl_();
498
    }
499
  }
500
501
  /**
502
   * Return an array with all cue points in the file, in the order they appear
503
   * in the file.
504
   * The difference between this method and using the list in WaveFile.cue
505
   * is that the return value of this method includes the position in
506
   * milliseconds of each cue point (WaveFile.cue only have the sample offset)
507
   * @return {!Array<!Object>}
508
   */
509
  listCuePoints() {
510
    /** @type {!Array<!Object>} */
511
    let points = this.getCuePoints_();
512
    for (let i = 0, len = points.length; i < len; i++) {
513
      points[i].milliseconds =
514
        (points[i].dwPosition / this.fmt.sampleRate) * 1000;
515
    }
516
    return points;
517
  }
518
519
  /**
520
   * Update the label of a cue point.
521
   * @param {number} pointIndex The ID of the cue point.
522
   * @param {string} label The new text for the label.
523
   */
524
  updateLabel(pointIndex, label) {
525
    /** @type {?number} */
526
    let cIndex = this.getAdtlChunk_();
527
    if (cIndex !== null) {
528
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
529
        if (this.LIST[cIndex].subChunks[i].dwName ==
530
            pointIndex) {
531
          this.LIST[cIndex].subChunks[i].value = label;
532
        }
533
      }
534
    }
535
  }
536
537
  /**
538
   * Make the file 16-bit if it is not.
539
   * @private
540
   */
541
  assure16Bit_() {
542
    this.assureUncompressed_();
543
    if (this.bitDepth != '16') {
544
      this.toBitDepth('16');
545
    }
546
  }
547
548
  /**
549
   * Uncompress the samples in case of a compressed file.
550
   * @private
551
   */
552
  assureUncompressed_() {
553
    if (this.bitDepth == '8a') {
554
      this.fromALaw();
555
    } else if (this.bitDepth == '8m') {
556
      this.fromMuLaw();
557
    } else if (this.bitDepth == '4') {
558
      this.fromIMAADPCM();
559
    }
560
  }
561
  
562
  /**
563
   * Push a new cue point in this.cue.points.
564
   * @param {number} position The position in milliseconds.
565
   * @param {number} dwName the dwName of the cue point
566
   * @private
567
   */
568
  setCuePoint_(position, dwName, label) {
569
    this.cue.points.push({
570
      dwName: dwName,
571
      dwPosition: position,
572
      fccChunk: 'data',
573
      dwChunkStart: 0,
574
      dwBlockStart: 0,
575
      dwSampleOffset: position,
576
    });
577
    this.setLabl_(dwName, label);
578
  }
579
580
  /**
581
   * Return an array with all cue points in the file, in the order they appear
582
   * in the file.
583
   * @return {!Array<!Object>}
584
   * @private
585
   */
586
  getCuePoints_() {
587
    /** @type {!Array<!Object>} */
588
    let points = [];
589
    for (let i = 0, len = this.cue.points.length; i < len; i++) {
590
      points.push({
591
        dwPosition: this.cue.points[i].dwPosition,
592
        label: this.getLabelForCuePoint_(
593
          this.cue.points[i].dwName)});
594
    }
595
    return points;
596
  }
597
598
  /**
599
   * Return the label of a cue point.
600
   * @param {number} pointDwName The ID of the cue point.
601
   * @return {string}
602
   * @private
603
   */
604
  getLabelForCuePoint_(pointDwName) {
605
    /** @type {?number} */
606
    let cIndex = this.getAdtlChunk_();
607
    if (cIndex !== null) {
608
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
609
        if (this.LIST[cIndex].subChunks[i].dwName ==
610
            pointDwName) {
611
          return this.LIST[cIndex].subChunks[i].value;
612
        }
613
      }
614
    }
615
    return '';
616
  }
617
618
  /**
619
   * Clear any LIST chunk labeled as 'adtl'.
620
   * @private
621
   */
622
  clearLISTadtl_() {
623
    for (let i = 0, len = this.LIST.length; i < len; i++) {
624
      if (this.LIST[i].format == 'adtl') {
625
        this.LIST.splice(i);
626
      }
627
    }
628
  }
629
630
  /**
631
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
632
   * @param {number} dwName The ID of the cue point.
633
   * @param {string} label The label for the cue point.
634
   * @private
635
   */
636
  setLabl_(dwName, label) {
637
    /** @type {?number} */
638
    let adtlIndex = this.getAdtlChunk_();
639
    if (adtlIndex === null) {
640
      this.LIST.push({
641
        chunkId: 'LIST',
642
        chunkSize: 4,
643
        format: 'adtl',
644
        subChunks: []});
645
      adtlIndex = this.LIST.length - 1;
646
    }
647
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
648
  }
649
650
  /**
651
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
652
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
653
   * @param {number} dwName The ID of the cue point.
654
   * @param {string} label The label for the cue point.
655
   * @private
656
   */
657
  setLabelText_(adtlIndex, dwName, label) {
658
    this.LIST[adtlIndex].subChunks.push({
659
      chunkId: 'labl',
660
      chunkSize: label.length,
661
      dwName: dwName,
662
      value: label
663
    });
664
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
665
  }
666
667
  /**
668
   * Return the index of the 'adtl' LIST in this.LIST.
669
   * @return {?number}
670
   * @private
671
   */
672
  getAdtlChunk_() {
673
    for (let i = 0, len = this.LIST.length; i < len; i++) {
674
      if (this.LIST[i].format == 'adtl') {
675
        return i;
676
      }
677
    }
678
    return null;
679
  }
680
681
  /**
682
   * Return the index of the INFO chunk in the LIST chunk.
683
   * @return {?number} the index of the INFO chunk.
684
   * @private
685
   */
686
  getLISTINFOIndex_() {
687
    /** @type {?number} */
688
    let index = null;
689
    for (let i = 0, len = this.LIST.length; i < len; i++) {
690
      if (this.LIST[i].format === 'INFO') {
691
        index = i;
692
        break;
693
      }
694
    }
695
    return index;
696
  }
697
698
  /**
699
   * Return the index of a tag in a FILE chunk.
700
   * @param {string} tag The tag name.
701
   * @return {!Object<string, ?number>}
702
   *    Object.LIST is the INFO index in LIST
703
   *    Object.TAG is the tag index in the INFO
704
   * @private
705
   */
706
  getTagIndex_(tag) {
707
    /** @type {!Object<string, ?number>} */
708
    let index = {LIST: null, TAG: null};
709
    for (let i = 0, len = this.LIST.length; i < len; i++) {
710
      if (this.LIST[i].format == 'INFO') {
711
        index.LIST = i;
712
        for (let j=0, subLen = this.LIST[i].subChunks.length; j < subLen; j++) {
713
          if (this.LIST[i].subChunks[j].chunkId == tag) {
714
            index.TAG = j;
715
            break;
716
          }
717
        }
718
        break;
719
      }
720
    }
721
    return index;
722
  }
723
724
  /**
725
   * Return 'RIFF' if the container is 'RF64', the current container name
726
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
727
   * @return {string}
728
   * @private
729
   */
730
  correctContainer_() {
731
    return this.container == 'RF64' ? 'RIFF' : this.container;
732
  }
733
}
734